const {
    warn,
    info,
    success,
    whisper,
    getPrint,
    setLastError,
    getLastError,
} = require("../utils/log_tool");
const { format, formatTimeInMillisec } = require("../utils/transform_tool");
const { BaseCommand } = require("./base");

const CODE2DESC = {
    0: "🟢 已连接",
    ETIMEDOUT: "⏰ 已超时",
    ECONNREFUSED: "⛔️ 被拒绝",
    ENOTFOUND: "🛑 未找到",
};

class WebSocketCommand extends BaseCommand {
    async execute() {
        const WebSocketClient = require("websocket").client;
        const url = this.selfData.url;
        const name = this.selfData.name || "";
        const name_postfix = name ? `(${name})` : "";
        const protocols = this.selfData.protocols; // || 'echo-protocol';
        // 字节顺序为 bigEndian（最高有效字节位于最前）或 littleEndian（最低有效字节位于最前）
        const little_endian = this.selfData.little_endian;
        /** @type {{type: string, name: string, bits: number}[]} */
        const headers = this.selfData.headers;
        /** @type {string[]} */
        const body_retrieval = this.selfData.body_retrieval;
        /** 生命周期，如果有设置，会在达到生命周期时，自动断开连接 */
        const lifetime = this.selfData.lifetime;
        const bodies = this.selfData.bodies;
        let receivedBytesTotal = 0;
        let receivedUTF8Total = 0;
        let receivedIndex = 0;
        const timeout = this.selfData.timeout || 0;
        const quiet = !this.selfData.verbose || true;
        const _endianSuffix = little_endian ? "LE" : "BE";

        let rl;
        let ok = true;
        let msg = "";
        await new Promise((resolve, reject) => {
            const startTime = Date.now();
            function resolveCode(code) {
                const elapsedTime = Date.now() - startTime;
                if (code) {
                    msg = `${CODE2DESC[code] || code} ${url} ${name_postfix} ${elapsedTime / 1000}s`;
                    setLastError(msg);
                } else {
                    msg = `${CODE2DESC[code]} ${url} ${name_postfix} ${elapsedTime / 1000}s`;
                    success(msg);
                }
            }
            var client = new WebSocketClient();
            const timer = timeout
                ? setTimeout(() => {
                      client.removeAllListeners("connect");
                      client.removeAllListeners("connectFailed");
                      client.abort();
                      resolveCode("ETIMEDOUT");
                      reject();
                  }, timeout)
                : 0;

            client.on("connectFailed", (err) => {
                if (timer) clearTimeout(timer);
                resolveCode(err.code);
                reject();
            });
            client.on("connect", (connection) => {
                if (timer) clearTimeout(timer);
                resolveCode(0);
                if (lifetime) {
                    setTimeout(() => {
                        !quiet && warn(`End of life cycle (${lifetime} ms)`);
                        connection.close(1000);
                    }, lifetime);
                }
                connection.on("error", (err) => {
                    msg = err;
                    setLastError(err);
                    reject();
                });
                connection.on("close", () => {
                    !quiet && warn("echo-protocol Connection Closed");
                    resolve();
                });
                connection.on("message", (message) => {
                    if (message.type === "utf8") {
                        receivedUTF8Total += message.utf8Data.length;
                        !quiet &&
                            whisper(
                                `[${receivedIndex++}] size=${message.utf8Data.length} total=${receivedUTF8Total}`,
                                " ",
                                true,
                            );
                        !quiet && info(message.utf8Data);
                    } else {
                        receivedBytesTotal += message.binaryData.length;
                        !quiet &&
                            whisper(
                                `[${receivedIndex++}] size=${message.binaryData.length}B total=${+(receivedBytesTotal / 1024).toFixed(2)}kB`,
                                " ",
                                true,
                            );
                        // info("[hex]" + message.binaryData.toString("hex"));
                        const unpacked = {};
                        let offset = 0;
                        offset = this.readStruct(
                            unpacked,
                            headers,
                            message.binaryData,
                            little_endian,
                            offset,
                        );
                        let body = bodies;
                        if (body_retrieval) {
                            for (const v of body_retrieval) {
                                if (!body) {
                                    break;
                                }
                                body = body[unpacked[v]];
                            }
                        }
                        if (body) {
                            body.title && info(body.title);
                            offset = this.readStruct(
                                unpacked,
                                body.struct,
                                message.binaryData,
                                little_endian,
                                offset,
                            );
                        }
                        !quiet && info(getPrint(unpacked, true));
                    }
                });
                /** @type {any[]} */
                const sends = this.selfData.sends;
                sends?.forEach((v) => {
                    this.send(connection, v);
                });

                this.readInput(
                    (v) => {
                        this.send(connection, v);
                    },
                    (v) => {
                        rl = v;
                    },
                );
            });
            !quiet &&
                warn(`Connecting websocket url=${url} protocols=${protocols}`);
            const options = Object.assign(
                {
                    rejectUnauthorized: false,
                },
                this.selfData.options,
            );
            client.connect(
                url,
                protocols,
                undefined,
                undefined,
                Object.assign(options),
            );
        }).catch((_reason) => {
            ok = false;
        });
        if (rl) {
            rl.close();
        }
        return [msg, ok];
    }

    /**
     *
     * @param {connection} connection
     * @param {string} v
     */
    send(connection, v) {
        const encodingIdx = v.indexOf(":");
        const encoding = v.substring(0, encodingIdx) || "utf8";
        const content = v.substring(encodingIdx + 1);
        info(`[send] ${content} encoding=${encoding}`);
        connection.sendBytes(Buffer.from(content, encoding));
    }

    /**
     *
     * @param {(v: string)=>any} resolve
     */
    readInput(resolve, retrieve) {
        const readline = require("node:readline");
        const rl = readline.createInterface({
            input: process.stdin,
            output: null,
        });
        rl.question("", (input) => {
            resolve(input);
            rl.close();
            this.readInput(resolve, retrieve);
        });

        rl.on("close", () => {});
        retrieve?.(rl);
    }

    /**
     *
     * @param {object} unpacked
     * @param {{type: string, name: string, bits: number, loop?: { name: string, struct: {type: string, name: string, bits: number}[] }}[]} structs
     * @param {Buffer} buffer
     * @param {boolean} little_endian
     * @param {number} offset
     * @returns
     */
    readStruct(unpacked, structs, buffer, little_endian, offset = 0) {
        const endianSuffix = little_endian ? "LE" : "BE";
        // let dataView = new DataView(new Uint8Array(buffer).buffer);
        structs?.forEach((struct) => {
            // let v = dataView[`get${struct.type}${struct.bits}`](offset, little_endian);
            let v;
            const bytes = struct.bits >> 3;
            if (struct.type === "String") {
                let end = offset + bytes;
                if (struct.trim) {
                    const idx = buffer.indexOf(0, offset);
                    if (idx > offset) {
                        end = idx;
                    }
                }
                v = buffer.toString(struct.encoding, offset, end);
            } else {
                v =
                    buffer[
                        `read${struct.type}${struct.bits}${struct.bits > 8 ? endianSuffix : ""}`
                    ](offset);
            }
            offset += bytes;
            if (struct.name) {
                unpacked[struct.name] = v;
            }
            if (struct.loop) {
                const loopv = [];
                for (let i = 0; i < v; ++i) {
                    const t = {};
                    offset = this.readStruct(
                        t,
                        struct.loop.struct,
                        buffer,
                        little_endian,
                        offset,
                    );
                    loopv.push(t);
                }
                unpacked[struct.loop.name] = loopv;
            }
        });
        return offset;
    }
}

module.exports = {
    WebSocketCommand,
};
